<?php

/**
 * API class for tr.im URL Shortening Service
 *
 * @author Andrew M. Whalen <trimapi@amwhalen.com>
 * @version 1.0
 * @copyright Copyright (c) 2010, Andrew M. Whalen. All rights reserved.
 * @license New BSD License
 *
 * Copyright (c) 2010, Andrew M. Whalen
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * - Neither the name of Andrew M. Whalen nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */
class TrimAPI {
	
	var $trim_api_url = 'http://api.tr.im/v1/';
	var $trim_port = '80';
	var $trim_api_key;
	var $username;
	var $password;
	var $debug;
	var $version = '1.0';
	
	/**
	 * Constructor
	 *
	 * @param string $user Your tr.im username.
	 * @param string $pass Your tr.im password.
	 * @param string $api The tr.im API Key (sometimes optional).
	 * @param boolean $debug Set to true if you want debugging output.
	 */
	function __construct($user, $pass, $api_key, $debug=false) {
		
		$this->username = $user;
		$this->password = $pass;
		$this->trim_api_key = $api_key;
		$this->debug = $debug;
		
	}
	
	/**
	 * Shortens the given long URL.
	 *
	 * @param string $long_url The URL to shorten with tr.im.
	 * @return string The shortened tr.im URL, returns false if there was an error.
	 */
	public function shorten($long_url) {
		
		$params = array(
			'url' => $long_url,
			'username' => $this->username,
			'password' => $this->password
		);
		
		$response = $this->doCall('trim_url.xml', $params);
		
		$xml = $this->getXML($response);
		
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			return (string)$xml->url;
		}
		
	}
	
	/**
	 * Returns the full URL that a tr.im URL points to.
	 *
	 * @param string $trim_url_path The unique part of a tr.im URL. E.g. for http://tr.im/abc use 'abc'.
	 * @return string The full URL that the tr.im URL points to, returns false if there was an error.
	 */
	public function expand($trim_url_path) {
		
		$params = array(
			'trimpath' => $trim_url_path,
		);
		
		$response = $this->doCall('trim_destination.xml', $params);
		
		$xml = $this->getXML($response);
		
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			return (string)$xml->destination;
		}
		
	}
	
	/**
	 * Returns the reference code for a tr.im URL if owned by the authenticated user.
	 *
	 * @param string $trim_url_path The unique part of a tr.im URL. E.g. for http://tr.im/abc use 'abc'.
	 * @return string The reference code for a tr.im URL, returns false if there was an error.
	 */
	public function getReference($trim_url_path) {
		
		$params = array(
			'trimpath' => $trim_url_path,
			'username' => $this->username,
			'password' => $this->password
		);
		
		$response = $this->doCall('trim_reference.xml', $params);
				
		$xml = $this->getXML($response);
		
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			return (string)$xml->reference;
		}
		
	}
	
	/**
	 * This method will attach an unattached tr.im URL to the authenticated account, if you have the unique reference code for the tr.im URL.
	 *
	 * @param string $trim_url_path The unique part of a tr.im URL. E.g. for http://tr.im/abc use 'abc'.
	 * @return boolean Returns true if the URL was successfully claimed by this authenticated account, returns false otherwise.
	 */
	public function claim($trim_url_path) {
		
		// first, get the URL's reference code if it's unowned
		$reference = $this->getReference($trim_url_path);
		
		$params = array(
			'reference' => $reference,
			'username' => $this->username,
			'password' => $this->password
		);
		
		$response = $this->doCall('trim_claim.xml', $params);
		
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			return true;
		}
		
	}
	
	/**
	 * Returns the domain names currently configured as domain name shortening services that tr.im  will not permit to be present within the destination URL.
	 *
	 * @return array The domain names that tr.im won't permit to be shortened, returns false if there was an error.
	 */
	public function shorteners() {
		
		$response = $this->doCall('shorteners.xml', array(), false);
		
		//return $response;
			
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			$domains = array();
			foreach ($xml->domains->domain as $d) {
				$domains[] = (string)$d;
			}
			return $domains;
		}
		
	}
	
	/**
	 * Returns the authenticated tr.im account's tr.im URLs with their visitation totals. No detailed visitation information is returned. This method is intended for generating an account's URL list. An API key is required to use this API method.
	 *
	 * @return array An array of URLs that belong to this account, returns false if there was an error.
	 */
	public function urls() {
		
		$params = array(
			'count' => 1000, // maximum of 100
			'username' => $this->username,
			'password' => $this->password
		);
		
		// must be accessed with GET
		$response = $this->doCall('account_urls.xml', $params, true, false);
					
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->status[0]->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			$urls = array();
			foreach ($xml->urls->item as $u) {
				$urls[] = array(
					'trimpath' => (string)$u->trimpath,
					//'reference' => (string)$u->reference,
					'destination' => (string)$u->destination,
					'visits' => (string)$u->visits,
					'trimmed' => (string)$u->trimmed,
				);
			}
			return $urls;
		}
		
	}
	
	/**
	 * Verifies whether or not the current username and password combination is a valid tr.im account's username and password.
	 *
	 * @return boolean Returns true if the username and password are correct, false otherwise.
	 */
	public function verify() {
		
		$params = array(
			'username' => $this->username,
			'password' => $this->password
		);
		
		$response = $this->doCall('verify.xml', $params, false);
				
		// get the returned status
		$xml = $this->getXML($response);
		$status = $xml->attributes();
		if ($status['result'] == 'ERROR') {
			return false;
		} else {
			return true;
		}
		
	}
	
	/**
	 * Returns an XML object if given an XML string. This validated the XML string and throws an exception if it's badly-formed XML.
	 *
	 * @param string $response The XML string.
	 * @return SimpleXMLElement The XML object specified by the XML string.
	 */
	private function getXML($response) {
		
		$xml = @simplexml_load_string($response);
		if ($xml === false) throw new TrimException('Invalid XML.');
		return $xml;
		
	}
	
	/**
	 * Gets a response in XML format from tr.im's api server.
	 *
	 * @return string The response XML as a string.
	 * @param string $url The URL path of the api method to request. E.g. trim_destination.xml
	 * @param array $parameters An associative array of parameters to pass for this request.
	 * @param boolean $authenticate Set to true to authenticate for this request.
	 * @param boolean $usePost Set to true to use a POST instead of a GET request.
	 */
	private function doCall($url, $parameters = array(), $authenticate = true, $usePost = true) {

		// build the request url
		$url = $this->trim_api_url . $url;

		// build the GET url if we're not using POST
		if (!empty($parameters) && !$usePost) {
			
			// setup with the api key
			if ($this->trim_api_key != '') {
				$query = '?api_key=' . $this->trim_api_key;
			} else {
				$query = '?';
			}
			
			// add all params to the query string
			$params = array();
			foreach ($parameters as $key => $value) {
				$params[] = urlencode($key) . '=' . urlencode($value);
			}
			$query .= implode('&', $params);

			// append to the request url
			$url .= $query;
			
		}

		// set options
		$options[CURLOPT_URL] = $url;
		$options[CURLOPT_PORT] = $this->trim_port;
		$options[CURLOPT_FOLLOWLOCATION] = true;
		$options[CURLOPT_RETURNTRANSFER] = true;
		$options[CURLOPT_TIMEOUT] = 60;

		// authenticate
		if ($authenticate) {
			$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
			$options[CURLOPT_USERPWD] = $this->username .':'. $this->password;
		}

		// if we're using POST, add the parameters here
		if (!empty($parameters) && $usePost) {
			
			$var = '';

			// rebuild parameters
			foreach($parameters as $key => $value) $var .= '&'. $key .'='. urlencode($value);

			// set extra options
			$options[CURLOPT_POST] = true;
			$options[CURLOPT_POSTFIELDS] = trim($var, '&');

			// The api server probably doesn't support the Expect: 100-continue header. So we reset it.
			$options[CURLOPT_HTTPHEADER] = array('Expect:');
			
		}

		// initialize cURL
		$curl = curl_init();

		// set options
		curl_setopt_array($curl, $options);

		// execute
		$response = curl_exec($curl);
		$headers = curl_getinfo($curl);

		// fetch errors
		$errorNumber = curl_errno($curl);
		$errorMessage = curl_error($curl);

		// close
		curl_close($curl);

		// validate body
		$xml = @simplexml_load_string($response);
		if ($xml !== false && isset($xml->error)) throw new TrimException((string) $xml->error);

		// invalid headers
		if (!in_array($headers['http_code'], array(0, 200))) {
			
			// provide debugging information
			if ($this->debug) {
				var_dump($url);
				var_dump($headers);
				var_dump($response);
			}

			// throw error
			throw new TrimException(null, (int) $headers['http_code']);
			
		}

		// error?
		if ($errorNumber != '') throw new TrimException($errorMessage, $errorNumber);

		// return
		return $response;
		
	}
	
}


/**
 * Tr.im Exception Class
 * @author Andrew M. Whalen
 */
class TrimException extends Exception {
	
	/**
	 * Tr.im HTTP Response Codes
	 */
	private $reponseCodes = array(
		200 => 'Request Completed. tr.im URL Added, if Applicable.',
		201 => 'Request Completed. tr.im URL Already Added for Session or Authenticated tr.im Account.',
		202 => 'Request Completed. tr.im URL Added and Attached to Authenticated tr.im Account.',
		205 => 'Request Completed. tr.im URL Added but NOT attached. Confirm Username/Password.',
		400 => 'Required Parameter URL Not Submitted.',
		401 => 'Submitted URL Invalid.',
		402 => 'Submitted URL is Already a Shortened URL.',
		403 => 'The URL has been Flagged as Spam and Rejected.',
		404 => 'The Custom tr.im URL Requested is Already in Use.',
		405 => 'Requested Custom URL Contains Invalid Characters.',
		406 => 'Requested Privacy Code Contains Invalid Characters.',
		407 => 'Requested Search Tags Contains Invalid Characters.',
		410 => 'Required Authentication Not Submitted or Invalid.',
		411 => 'URL Reference Code Not Submitted.',
		412 => 'URL Reference Code Not Does Not Exist.',
		413 => 'tr.im URL Path Not Submitted.',
		414 => 'tr.im URL Does Not Exist.',
		415 => 'tr.im URL Referenced Already Claimed.',
		420 => 'Media Type Uploaded Not Supported.',
		421 => 'Media Uploaded too Large.',
		422 => 'Media Uploaded XY Dimensions too Small.',
		425 => 'API Rate Limit Exceeded.',
		426 => 'API Key Submitted Does Not Exist or is Invalid.',
		427 => 'API Key Required.',
		445 => 'Parameter Data Within the Request is Invalid.',
		446 => 'Required Parameter Missing within the Request.',
		450 => 'An Unknown Error Occurred. Please email api@tr.im.'
	);
	
	/**
	 * Constructor
	 *
	 * @return void
	 * @param string $message The status message.
	 * @param int $code The HTTP code number.
	 */
	public function __construct($message = null, $code = null) {
		
		// set message
		if ($message === null && isset($this->reponseCodes[(int) $code])) {
			$message = $this->reponseCodes[(int) $code];
		}

		// call parent
		parent::__construct((string) $message, $code);
		
	}
	
}



?>